import lodash from 'lodash'
import jquery from 'jquery'
import http from 'axios'
import Qs from 'qs'

import br from './util/browser.js'
import uid from './util/uid.js'
import loc from './util/loc/zh-CN.js'
import {number, isNumber, numRange, numAdd, numFormat, numDecimal} from './util/number.js'
import {strTrim, strEscape, strFrom, strTo, strPad, strLen, strFormat, strHighlight} from './util/str.js'
import {dateFormat, dateParse, dateAdd, dateValid, dateDiff, dateSheet, dateWeek, dateWeekFirst, dateWeekLast} from './util/date.js'
import {rngSelection, rngElement, rngCursor, rngCursorOffset, rngText} from './util/range.js'
import {el, html, append, prepend, before, after, replaceWith} from './util/elem.js'
import {bcr, offset, snap, snapTo} from './util/snap.js'
import {EventListener, eventX, eventY, moveup} from './util/event.js'


function $(o) {
  if (!o) return
  if (('isWidget' in o) && o.isWidget) return o
  if (o.$attrs && o.$attrs.templateWrapper) {
    return $.vueCache[o.$attrs.templateWrapper.uid]
  }
  if (o.uid) return $.vueCache[o.uid]
  if (o instanceof Event) {
    o = o.currentTarget || o.target
  }
  if (o.$el) {
    o = o.$el
    if (o.nodeType === 3)
      o = o.nextElementSibling
  }
  if (lodash.isElement(o)) {
    do {
      if (o.id) {
        if ($.vueCache[o.id]) return $.vueCache[o.id]
        let d = $.strTo(o.id, ':') + ':'
        if($.vueCache[d]) return $.vueCache[d]
      }
    } while (o = o.parentNode)
  }
  while (o && !o.isWidget) o = o.$parent
  return o || $.docView
}

Object.assign($, {
  x: {defaultOptions: {}},
  vueCache: {},
  viewCache: {},
  dfclsCache: {},
  q: jquery, lodash, http,
  br, loc, uid, EventListener, eventX, eventY,
  number, isNumber, numRange, numAdd, numFormat, numDecimal,
  strTrim, strEscape, strFrom, strTo, strPad, strLen, strFormat, strHighlight,
  dateFormat, dateParse, dateAdd, dateValid, dateDiff, dateSheet, dateWeek, dateWeekFirst, dateWeekLast,
  rngSelection, rngElement, rngCursor, rngCursorOffset, rngText,
  el, html, append, prepend, before, after, replaceWith,
  moveup, bcr, offset, snap, snapTo,
})

// a继承b的属性。不覆盖a的既有同名属性
$.extend = function(a, ...args) {
  args.forEach(c => {
    if (c)
      for (let k in c)
        if (!(k in a) && c[k] !== undefined) a[k] = c[k]
  })
  return a
}

// 递归继承
$.extendDeep = function(a, ...args) {
  args.forEach(c => {
    if (c != null && c.constructor === Object) {
      for (let i in c) {
        if (!(i in a)) {
          if (c[i] != null && c[i].constructor === Object) {
            $.extendDeep(a[i] = {}, c[i])
          } else
            a[i] = c[i]
        } else if (a[i] != null && a[i].constructor === Object)
          $.extendDeep(a[i], c[i])
      }
    }
  })
  return a
}

$.merge = function(a, ...args) {
  args.forEach(c => {
    if (c)
      for (let k in c) a[k] = c[k]
  })
  return a
}

$.mergeDeep = function(a, ...args) {
  args.forEach(c => {
    if (c != null) {
      for (let k in c) {
        if (typeof a[k] === 'object')
          $.mergeDeep(a[k], c[k])
        else
          a[k] = c[k]
      }
    }
  })
  return a
}

$.cloneDeep = lodash.cloneDeep

$.eval = function(str, opt) {
  if (opt) {
    let keys = Object.keys(opt)
    return Function(keys.map(k => k), `return (${str})`)(...keys.map(k => opt[k]))
  }
  return Function(`return (${str})`)()
}

// a -> object, b -> concat?
$.array = function(a, b) {
  return Array.isArray(a) ? (b ? a.concat() : a) : a == null || a === '' ? [] : typeof a === 'string' ? a.split(',') : [a]
}

// 数字转为像素单位。如果自带单位则保留单位
$.unitSize = (size) => size == null || size === '' ? '' : size === '*' ? '' : size == -1 || size == 'auto' ?
  'auto' : isNaN(size) ? size : `${size}px`

// 添加一个ID
$.idsAdd = (s, n, p) => {
  s = s == null ? '' : '' + s;
  n = n == null ? '' : '' + n;
  p = p == null ? ',' : '' + p;
  if (!s || !n) return s || n;
  for (var i = 0, n = n.split(p), l = n.length; i < l; i++) {
    if ((p + s + p).indexOf(p + n[i] + p) < 0) s += (s ? p : '') + n[i];
  }
  return s
}
// 删除一个ID
$.idsRemove = (s, n, p) => {
  s = s == null ? '' : '' + s;
  n = n == null ? '' : '' + n;
  p = p == null ? ',' : '' + p;
  if (!s || !n) return s;
  for (var i = 0, e, n = n.split(p), l = n.length; i < l; i++) {
    do {
      e = (p + s + p).indexOf(p + n[i] + p);
      if (e > -1) s = s.slice(0, e ? e - 1 : e) + s.slice(e + n[i].length + (e ? 0 : 1));
    } while (s && e > -1);
  }
  return s
}
// s是否包含n。如果 n 也是逗号隔开，那么只需n中有匹配到s中的一项即返回true
$.idsAny = (s, n, p) => {
  if (s === '' && n === s) return true
  s = s == null ? '' : '' + s
  n = n == null ? '' : '' + n
  p = p == null ? ',' : '' + p
  if (!s || !n) return false
  if (s == n) return true
  for (let v of n.split(p))
    if ((p + s + p).indexOf(p + v + p) > -1) return true
  return false
}

// s是否包含n。如果 n 也是逗号隔开，那么只需n中有匹配到s中的一项即返回true
$.idsAll = (s, n, p) => {
  if (s === '' && n === s) return true
  s = s == null ? '' : '' + s
  n = n == null ? '' : '' + n
  p = p == null ? ',' : '' + p
  if (!s) return false
  if (!n || s == n) return true
  for (let v of n.split(p))
    if ((p + s + p).indexOf(p + v + p) < 0) return false
  return true
}

// 添加一个class  /@ a -> el, b -> className, c -> add?(默认添加)
$.classAdd = function(a, b, c) {
  if (!a) return
  let s = a.className
  if (b.indexOf(' ') > -1) {
    b = $.strTrim(b).split(/ +/)
    for (let i = 0, l = b.length; i < l; i++)
      s = (c === false ? $.idsRemove : $.idsAdd)(s, b[i], ' ')
  } else
    s = (c === false ? $.idsRemove : $.idsAdd)(s, b, ' ')
  a.className = s
}

// 删除一个class  /@ a -> el, b -> className
$.classRemove = function(a, b) {
  if (!a) return
  if (b.indexOf(' ') > -1) {
    $.classAdd(a, b, false)
  } else
    a.className = $.idsRemove(a.className, b, ' ')
}

// 是否包含任意一个class(多个是“或”的关系)  /@ a -> el or className
$.classAny = function(a, b) {
  if (!a) return
  let s = typeof a === 'string' ? a : a.className
  if (b.indexOf(' ') > -1) {
    b = $strTrim(b).split(/ +/)
    for (let i = 0, l = b.length; i < l; i++)
      if ($.idsAny(s, b[i], ' ')) return i + 1
  } else
    return $.idsAny(s, b, ' ')
}

// url 编码
$.urlEncode = (a) => {
  return a == null ? '' : encodeURIComponent(typeof a === 'object' ? JSON.stringify(a) : a)
}
  
// url 解码
$.urlDecode = (a) => {
  return a == null ? '' : decodeURIComponent(a.replace(/\+/g, ' '))
}
// 替换带$变量的url，如 "a.sp?id=$0" /@a -> url, b -> array/object
$.urlFormat = (a, ...args) => {
  args.forEach((v, i) => v != null && (a = $.strFormat(a, v, true, i < args.length - 1)))
  return a
}
$.urlParam = function(a, b) {
  var r = a.split('#'),
    u = r[0],
    h = r[1];
  if (typeof b === 'string') {
    return b === '#' ? (h || '') : $.urlDecode((u.match(new RegExp('[\\?&]' + b + '=([^&]*)'), 'g') || A)[1]);
  } else if (b && typeof b === 'object') {
    for (var k in b) {
      if (k === '#') {
        h = b[k];
      } else {
        var c = $.urlEncode(b[k]),
          d = new RegExp('\\b' + k + '=[^&]*');
        u = d.test(u) ? u.replace(d, k + '=' + c) : u + (u.indexOf('?') < 0 ? '?' : '&') + k + '=' + c;
      }
    }
  } else if (arguments.length === 1) {
    var r = {},
      c = u.split('?');
    c = c[c.length - 1];

    if (c) {
      c = c.split('&');
      for (var d = 0, e, f, g; d < c.length; d++) {
        e = c[d].indexOf('=');
        if (e == -1) continue;
        f = c[d].substring(0, e);
        g = c[d].substring(e + 1);
        r[f] = $.urlDecode(g.replace(/\+/g, ' '));
      }
    }
    return r
  }
  return u + (h ? '#' + h : '')
}
// 把相对路径url和baseURL拼接为完整地址
$.urlServer = function(src, ...args) {
  if (!src) return
  if (!(src.startsWith('http://') || src.startsWith('https://'))) {
    let b = lodash.get($.x, 'http.baseURL')
    if (b) {
      src = b + (b.endsWith('/') ? '' : '/') + src
    }
  }
  return $.strFormat(src, ...args)
}

$.urlComplete = function(a) {
	if (a.indexOf('data:') !== 0) {
		a = $.urlServer(a)
    let r = lodash.get($.x, 'http.params')
		r && (a = $.urlParam(a, r))
	}
	return a
},
	// @a -> src, b -> options
$.download = function(a, b) {
  a = $.urlComplete(a)
	if (br.app) {
		$.wifiConfirm(function() {
			let dtask = plus.downloader.createDownload(a, b)
			plus.nativeUI.showWaiting()
			dtask.addEventListener("statechanged", function(task, status) {
				switch (task.state) {
					case 1: // 开始
						plus.nativeUI.toast($.loc.download_start)
						break
					case 4: // 下载完成
						if (status == 200) {
							plus.nativeUI.toast($.loc.download_complete)
							plus.runtime.openFile(task.filename, {}, function(e) {
								plus.nativeUI.alert($.loc.download_cannotopen)
							})
						} else {
							plus.nativeUI.toast($.loc.download_fail + status)
						}
						plus.nativeUI.closeWaiting()
						break
				}
			})
			dtask.start()
		})
	} else {
		let link = document.createElement('a')
    //link.download = true
    link.href = a
    if ($.x.downloadTarget) {
      link.target = $.x.downloadTarget
    }
    link.click()
    link.remove()
	}
}


$.wifiConfirm = function(a, b) {
	$.x.wifiConfirm && plus.networkinfo.getCurrentType() != 3 ? plus.nativeUI.confirm($.loc.connection_not_wifi, function(e){e.index === 0 && a.call(b)}, {buttons:[$.loc.confirm, $.loc.cancel]}) : a.call(b)
}

$.vm = (a) => {
  if (!a) return $.docView
  if (typeof a === 'string') {
    if ($.viewCache[a]) return $.viewCache[a]
    if ($.vueCache[a]) {
      a = $.vueCache[a]
      if (a.isView) return a
    }
  }
  a = $(a)
  if (a == null) return
  if (a.isEntity) {
    return a.isView ? a : a.ownerView
  }
  return a.isView ? a : a.ownerView
}

$.dialog = (a) => {
  if (!a) return
  a = $.vm(a)
  while (a && !a.isDialog) a = a.ownerView
  return a
}

$.close = (a) => {
  let d = $.dialog(a)
  d && d.close()
}

$.closePopup = () => {
  document.dispatchEvent(new Event('mousedown'))
}

$.scrollIntoView = (a) => {
  if (!a) return;
  let e = a.isWidget ? a : $(a)
  if (e.onbeforescrollintoview) e.onbeforescrollintoview()
  let s = jquery(a.isWidget ? a.el() : a).closest('.w-scroll-y,.w-scroll-x');
  if (!s.length) return;
  const scroll = () => {
    let o = s && $(s[0])
    if (o && o.scrollTo) {
      let b = bcr(s[0])
      let c = bcr(a)
      let x = (c.left + c.right) / 2
      let y = (c.top + c.bottom) / 2
      if (x < b.left || x > b.right || y < b.top || y > b.bottom) {
        o.scrollTo(a, 'middle', 'center')
      }
    }
  }
  setTimeout(scroll, 50)
}

$.previewVideo = function(src) {
  $.vm().previewVideo($.urlServer(src))
}
	// @img -> image array, options -> {current:i,loop:true,indicator: 'number'}
$.previewImage = function(imgs, options) {
  imgs = $.array(imgs).map(v => $.urlServer(v))
	if (br.app) {
		plus.nativeUI.previewImage(imgs, options)
	} else {
    $.vm().previewImage(imgs, options)
	}
}

$.slotsChildren = function(slots, name = 'default') {
  let r = []
  const getChildren = (d) => {
    d && d.forEach(v => {
      if (typeof v.type === 'symbol') {
        Array.isArray(v.children) && getChildren(v.children)
      } else
        r.push(v)
    })
  }
  getChildren(slots[name] && slots[name]())
  return r
}

$.hasChild = function(slots, name = 'default') {
  let r = false
  const getChildren = (d) => {
    lodash.forEach(d, v => {
      if (typeof v.type === 'symbol') {
        Array.isArray(v.children) && getChildren(v.children)
      } else {
        return !(r = true)
      }
    })
  }
  getChildren(slots[name] && slots[name]())
  return r
}

// @a -> widget|HTMLElement, y -> {print: T/F(是否打印), head: "head标签", input2text: T/F(把表单值转为文本)
$.print = function(a, y) {
		let d = a.el ? a.el() : (a.$el || a), w = d.ownerDocument, b = $.q('link[media=print]', w), c = [];
    y = y || {};
		$.q('meta', w).each(function() {c.push(this.outerHTML)});
		(b.length > 0 ? b : $.q('link', w)).each(function() {c.push('<link rel="stylesheet" type="text/css" href="' + this.getAttribute('href') + '"/>');});
		$.q('style', w).each(function() {c.push(this.outerHTML)});
		let s = d.outerHTML;
		s = s.replace(/<div[^>]+w-scroll[^>]+>/gi, function($0) {return $0.replace(/height: \w+/gi, '');});
		$.q(':text,:password,textarea,select', d).each(function() {
			let h = this.outerHTML, v, r;
			if (y.input2text) {
				let g = $(this), w = 'auto';
				if (g && g.isFormWidget) {
					h = g.el('main').outerHTML;
					v = g.formText();
				} else
					v = this.value;
				v = '<div class="f-oh w-input-s">' + v + '</div>';
			} else {
				v = h.replace('value="' + this.defaultValue + '"', () => 'value="' + this.value + '"');
			}
			s = s.replace(h, () => v + (r || ''));
		});
		$.q(':radio,:checkbox', d).each(function() {
			if (y.input2text) {
				let g = $(this);
				if (g && g.isFormWidget) {
					s = s.replace(g.el().outerHTML, () => g.isChecked ? '<div class="f-wdbr">' + g.text + '</div>' : '');
				}
			} else {
				let h = this.outerHTML, n = h.replace(/ checked=""/, '');
				if (this.checked)
					n = n.replace(/>$/, ' checked>');
				s = s.replace(h, n);
			}
		});
		if (y.input2text) {
			$.q('input[type=hidden]', d).each(function() {
				var g = $(this);
				if (g && g.isFormWidget && g.$options.name != 'w-hidden') {
					s = s.replace(g.el('main').outerHTML, () => '<div class="f-wdbr f-print-input-text">' + g.formText() + '</div>');
				}
			});
		}
		$.q('.w-input-placeholder', d).each(function() {
			s = s.replace(this.outerHTML, '');
		});
    const printf = function() {
    	let w = window.frames.wPrintFrame;
    	if (!w) {
    		$.q('<div class=f-none><iframe name=wPrintFrame></iframe></div>').appendTo(document.body);
    		w = window.frames.wPrintFrame;
    	}
    	return w;
    }
    w = y.print !== false ? printf() : window.open();
		d = w.document;
		d.open('text/html', 'replace');
		d.write('<!doctype html><html class=f-print><head><meta charset=utf-8><title>' + $.loc.print_preview +
			'</title><style>.f-print .w-f-remark{height:auto;line-height:inherit}.f-print-input-text{line-height: var(--w-label-min-height);}.w-input{display: flex!important;}.w-input-s{word-break:normal;white-space:normal}.w-input-s:empty:before{content:"\xa0"}</style><script>var $={e:function(){}}</script>' + c.join('') +
			(y.head || '') + '</head><body><div class=x-print>' + s + '</div>' +
			(y.print !== false ? '<script>window.print();window.close()</script>' : '') +
			'</body></html>');
		d.close();
		return d;
	}


const queryEncode = function(data, config) {
  if (!data || data instanceof FormData) return data
  let contentType = $.lodash.get(config, 'headers["Content-Type"]') || $.lodash.get($.x, 'http.headers.post["Content-Type"]')
  return contentType == 'application/x-www-form-urlencoded' ? Qs.stringify(data, {arrayFormat: 'repeat'}) : data
}

$.ajax = function(cmd, args) {
  let src = args ? $.urlFormat(cmd.src, args) : cmd.src
  if (!src) {
    return Promise.reject('$.ajax(): The first parameter must be an object, and the property(src) of the object is required.')
  }
  let ctx = cmd.context || $.docView
  let lock = ctx.ajaxLock
  let config = $.lodash.pick(cmd, ['headers', 'responseType'])
  if (cmd.download) {
    $.extend(config, {responseType: 'blob'})
  }
  lock && lock()
  cmd.loading && ctx.exec({type: 'Loading'}, null, cmd.loading)
  return (cmd.data ? $.http.post(src, queryEncode(cmd.data, config), config) : $.http.get(src, config))
    .then(res => {
      lock && lock(false)
      let data = res.data
      if (cmd.filter) data = cmd.filter(res, ctx)
      if (cmd.success) cmd.success(data, ctx)
      if (cmd.complete) cmd.complete(res, ctx)
      cmd.loading && ctx.cmd({type: 'Loading', hide: true})
      if (cmd.download) {
        let disposition = res.headers['content-disposition']
        if (disposition) {
          let fileName = decodeURI(disposition.substring(disposition.indexOf('filename=') + 9, disposition.length))
          let blob = new Blob([res.data])
          let link = document.createElement('a')
          link.href = URL.createObjectURL(blob)
          link.download = fileName
          link.click()
          link.remove()
        } else {
          console.error('download: none content-disposition')
        }
      }
      return Promise.resolve(data)
    }).catch(ex => {
      lock && lock(false)
      if (cmd.error) cmd.error(ex, ctx)
      if (cmd.complete) cmd.complete(ex.response, ctx, ex)
      cmd.loading && ctx.exec({type: 'Loading'}, null, cmd.loading)
      return Promise.reject(ex)
    })
}

$.extend($.ajax, {
  get(src, config) {
    return $.http.get(src, config).then(res => new Promise(resolve => resolve(res.data)))
  },
  post(src, data, config) {
    return $.http.post(src, queryEncode(data, config), config).then(res => new Promise(resolve => resolve(res.data)))
  }
}, http)

$.cmdHooks = {
  Submit(cmd, args) {
    let src = $.urlFormat(cmd.src, args)
    const vm = $.vm(this)
    const submit = () => new Promise(rsv => this.$nextTick(() => {
      let d = cmd.validate || false
      let f = vm.getFormList(d.range || cmd.range)
      let g = d.range && d.range != cmd.range ? vm.getFormList(cmd.range) : f
      if (vm.valid(f, d.group)) {
        cmd.context = cmd.context || this
        let post = vm.getPostData(g)
        cmd.data = cmd.data ? $.extend(cmd.data, post) : post
        $.ajax(cmd, args).then(data => rsv(data))
      }
    }))
    if (vm.isEntity) {
      return submit()
    } else {
      return new Promise(rsv => {
        vm.addEvent('load', () => submit().then(data => rsv(data)))
      })
    }
  },
  Ajax(cmd, args) {
    cmd.context = cmd.context || this
    return $.ajax(cmd, args)
  },
  Dialog(cmd, args) {
    if (cmd.title) cmd.title = $.strFormat(cmd.title, args)
    return $.vm(this).cmdDialog(cmd, args, this)
  },
  Alert(cmd, args) {
    if (cmd.text) cmd.text = $.strFormat(cmd.text, args)
    return $.vm(this).cmdDialog(cmd, args, this)
  },
  Confirm(cmd, args) {
    if (cmd.text) cmd.text = $.strFormat(cmd.text, args)
    return $.vm(this).cmdDialog(cmd, args, this)
  },
  Tip(cmd, args) {
    if (cmd.text) cmd.text = $.strFormat(cmd.text, args)
    return $.vm(this).cmdDialog(cmd, args, this)
  },
  Menu(cmd, args) {
    return $.vm(this).cmdDialog(cmd, args, this)
  },
  Loading(cmd, args) {
    return $.vm(this).cmdDialog(cmd, args, this)
  },
}

$.isCmd = function(a) {
  return a && a.type && $.cmdHooks[a.type]
}

$.alert = function(text, position, timeout, yes) {
  $.docView.cmd(typeof text === 'object' ? {type: 'Alert', title: '操作提示', ...text} : {
    type: 'Alert', title: '操作提示', text, position, timeout, yes
  })
}

$.confirm = function(text, yes, no, cover) {
  $.docView.cmd(typeof text === 'object' ? {type: 'Confirm', title: '操作提示', ...text} : {
    type: 'Confirm', title: '操作提示', text, yes, no, cover
  })
}

$.directiveHooks = {
  colspan(el, v, n) {
    el.style.gridColumn = !v || v == -1 ? '' : `span ${v}`
  },
  rowspan(el, v, n) {
    el.style.gridRow = !v || v == -1 ? '' : `span ${v}`
  }
}

$.config = function(opt) {
  $.__config_uid = uid() //读取默认配置的地方有缓存，根据这个uid来判断是否刷新缓存
  $.mergeDeep($.x, opt)
  if (opt.http) {
    $.mergeDeep(http.defaults, opt.http)
    http.interceptors.response.use(
      opt.http.filter,
      opt.http.error || function(ex) {
        if (ex.response) {
          ex.callback = () => $.alert((ex.response.status) + ': ' + ex.config.url)
        }
        return Promise.reject(ex)
      }
    )
  }
  const _dfopt = $.x.defaultOptions
  const _dfcls = {}
  for (let k in _dfopt) {
    if (k.includes('.')) {
      let a = $.strFrom(k, '.').replace(/\./g, ' ')
      let b = $.strTo(k, '.');
      ($.dfclsCache[b] || ($.dfclsCache[b] = {}))[a] = _dfopt[k]
    }
  }
}

window.addEventListener('unhandledrejection', (event) => {
  if ($.x.catchReject) {
    $.x.catchReject(event)
  } else {
    if (event.reason) {
      if (event.reason.callback)
        event.reason.callback()
      else if (typeof event.reason === 'string')
        console.error(event.reason)
    }
    event.preventDefault()
  }
})

export default $
